﻿using System;
using System.Collections.Generic;
using System.Data;

/*
	The Controller class of a model/view/controller pattern
	The goal is that the view class contains no logic that acts on
	the model. It only binds controls to model data.
	the controller class has no idea about the controls on the interface
	and the model class is purely a data-store with no logic.

	To achieve this we are binding properties of the model to 
	controls in the view via the controller. 
*/

namespace DbView
{
	public class LookupEditController
	{
		// > Boilerplate: references to objects of the pattern
		// keep the original safe until the Save button is pressed
		// compare with the new to set the "dirty" flag 
		public LookupModel model;		// original
		public LookupModel newModel;	// model boud to controls

		public List<string> Tables {get; set;}  // list of availiable tables for linking 
		public List<string> SourceFields {get; set; }  // list of available fields in source table selected

		public List<string> LookupFields {get; set;}		// Lookup table fields
		// these are the target so they are in the model
		// public List<string> SelectedFields {get; set;} 

		// objects to bind to the current list control selections
		// Lookup field selection 
		public string Filter {get; set;}
		// lookup table left hand list box selected
		public string LookupFieldSelection { get; set; }
		// lookup table right hand list box selected
		public string SelectedLookupsSelection { get; set; }

		// friendly name for the model for the view to access
		public LookupModel Model
		{
			get { return newModel; }
		}

		// reference the view as there are messages to pass from one to the other
		public LookupEditForm View { get; set; }
		// < Boilerplate: references to objects of the pattern.

		private DbViewDriver.ISchemaInfo infoIface;
		// add objects to be bound to controls in the view
		// Note: normally the model properties are bound,
		// but for collection controls it is slightly more involved
//		List<string>tables = new List<string>();

		string lastTT = "";
		string lastError = "";

		// Constructor
		//
		public LookupEditController(DbViewDriver.ISchemaInfo iInfo, LookupModel modelIn)
		{
			// > Boilerplate: Constructor work
			// hold a reference to the original model. 
			this.model = modelIn;
			// create a working copy of the original
			this.newModel = new LookupModel();
			this.newModel.Copy(this.model);
			// < Boilerplate: Constructor work
			this.infoIface = iInfo;

//			this.tables.AddRange(this.infoIface.GetTables());
			this.Tables = new List<string>();
			this.Tables.AddRange(this.infoIface.GetTables());
			this.SourceFields = new List<string>();
			this.LookupFields = new List<string>();
			// this is working around a control behaviour problem. 
			// keep the last target table to preserve previously selected fields
			this.lastTT = this.newModel.LookupTable;
		}

		// The view calls this either at the end of construction or (better)
		// when the form is loaded, just before it is displayed.
		// Its a fairly standard method, but specialised for each dialog.
		public void SetFormInitialState()
		{
			List<string> source;
			if (string.IsNullOrEmpty(this.newModel.Table))
			{
				source = this.FilterTheSelectionList("", this.Tables, new List<string>());
				this.View.RefreshSourceTable(source);
			}

			source = this.FilterTheSelectionList("", this.Tables, new List<string>());
			this.View.RefreshLookupTable(source);

			// sets the lookup fields to select
			if (!string.IsNullOrEmpty(this.newModel.LookupTable))
				this.SetSelectionFieldList(this.newModel.LookupTable, (lastTT == this.newModel.LookupTable));
		}

		// > Boilerplate: Have changes been made to the working copy of the model?
		public bool IsDirty()
		{
			return !(newModel.Equals(model));
		}
		
		// Either called from the view or from the controller
		// This simple example does not require a validation step.
		public bool CommitChangesToModel()
		{
			// validate and return false if necessary
			bool IsValid = ValidateModel(this.newModel);
			if (IsValid)
				this.model.Copy(this.newModel);
			return IsValid;
		}

		private bool ValidateModel(LookupModel model)
		{
			// no item must be empty
			if (string.IsNullOrEmpty(model.Table)) return this.SetError("Source table cannot be empty");
			if (string.IsNullOrEmpty(model.Field)) return this.SetError("Source field cannot be empty");
			if (string.IsNullOrEmpty(model.LookupTable)) return this.SetError("Lookup table cannot be empty");
			if (string.IsNullOrEmpty(model.LookupField)) return this.SetError("Lookup field cannot be empty");
			if (!this.Tables.Contains(model.Table)) return this.SetError("Invalid Source table name selected");
			if (!this.Tables.Contains(model.LookupTable)) return this.SetError("Invalid Lookup table name selected");
			if (this.SourceFields.Count > 0) // if 0 then field has been sent into the dialog
				if (!this.SourceFields.Contains(model.Field)) return this.SetError("Invalid source field selected");
			if (!this.LookupFields.Contains(model.LookupField)) return this.SetError("Invalid Lookup field selected");
			if (model.SelectedFields.Count == 0) return this.SetError("Please select at least one field for the lookup display");
			// add check of type 
			DbView.FieldInfo [] fields = this.infoIface.GetSchema(model.Table);
			DbView.FieldInfo f1 = this.GetFieldInfoFor(fields, model.Field);
			fields = this.infoIface.GetSchema(model.LookupTable);
			DbView.FieldInfo f2 = this.GetFieldInfoFor(fields, model.LookupField);
			if (f1 != null & f2 != null && f1.Type != f2.Type)
				return this.SetError(string.Format("Selected fields are of a different type: {0}.{1} = {2} {3}.{4} = {5}", 
													model.Table, model.Field, f1.Type, model.LookupTable, model.LookupField, f2.Type));
			// set type 
			if (f1 != null) 
				model.FieldType = f1.Type;
			this.lastError = "";
			return true;
		}
		
		DbView.FieldInfo GetFieldInfoFor(DbView.FieldInfo [] fields, string fieldName)
		{
			foreach (DbView.FieldInfo fi in fields)
			{
				if (fi.Name == fieldName)
					return fi;
			}
			return null;
		}


		bool SetError(string text)
		{
			this.lastError = text;
			return false;
		}

		public string GetLastError()
		{

			return this.lastError;
		}
		// < Boilerplate.
	
		///////////////////////////////////////////////////////////////////////////////////////////////
		// API for the view to consume. 
		internal void OnLookupTableSelected()
		{
			// if (this.updating) return;
			this.SetSelectionFieldList(this.newModel.LookupTable, (lastTT == this.newModel.LookupTable));
		}

		internal void OnSourceTableSelected()
		{
			// test whether this is the same?
			DbView.FieldInfo [] fields = this.infoIface.GetSchema(this.newModel.Table);
			SetSourceFieldList(this.newModel.Table);
		}

		internal void OnAdd()
		{
			string pi = this.LookupFieldSelection;
			if (pi != null)
			{
				// get current list applicable to the source list (this is necessary here
				// to work out the correct item to set the cursor at.)
				List<string> source = this.FilterTheSelectionList(this.Filter);
				// add the selection to the selected list
				this.newModel.SelectedFields.Add(pi);
				// work out the new source selected item based on the item to be remove
			    string piSel = FindNewSelection(source, pi);
				// remove it
				source.Remove(pi);
				// set the new selection *before* refreshing the view with the adjusted list
				this.LookupFieldSelection = piSel;
				this.View.RefreshSelectionFields(source);

				// for the selected items we need to refresh the controls to redisplay the list
				this.SelectedLookupsSelection = pi;
				this.View.RefreshSelectedFields(this.newModel.SelectedFields);
			}
		}

		// Delete button pressed
		internal void OnDelete()
		{
			string pi = this.SelectedLookupsSelection;
			if (pi != null)
			{
				// work out which item to select when the current item is removed
				string piSel = FindNewSelection(this.newModel.SelectedFields, pi);
				// remove it.
				this.newModel.SelectedFields.Remove(pi);
				// for the selected items we need to refresh the controls to redisplay the list
				this.SelectedLookupsSelection = piSel;
				this.View.RefreshSelectedFields(this.newModel.SelectedFields);

				// set the selection on the source list to be the item just added back
				this.LookupFieldSelection = pi;
				this.View.RefreshSelectionFields(this.FilterTheSelectionList(this.Filter));
			}
		}

		internal void OnMoveUp()
		{
			// for the selected items we need to refresh the controls to redisplay the list
			int idx = this.newModel.SelectedFields.IndexOf(this.SelectedLookupsSelection);
			if (idx > 0)
			{
				this.newModel.SelectedFields.RemoveAt(idx--);
				this.newModel.SelectedFields.Insert(idx, this.SelectedLookupsSelection);
				this.View.RefreshSelectedFields(this.newModel.SelectedFields);
			}
		}
		internal void OnMoveDown()
		{
			// for the selected items we need to refresh the controls to redisplay the list
			int idx = this.newModel.SelectedFields.IndexOf(this.SelectedLookupsSelection);
			if (idx < this.newModel.SelectedFields.Count - 1)
			{
				this.newModel.SelectedFields.RemoveAt(idx++);
				this.newModel.SelectedFields.Insert(idx, this.SelectedLookupsSelection);
				this.View.RefreshSelectedFields(this.newModel.SelectedFields);
			}
		}

		internal void OnFilterChanged()
		{
			// find items to display in the source list
			List<string> source = this.FilterTheSelectionList(this.Filter);
			// set the item to select. If none then we cannot. If some then set to 1st
			if (source.Count > 0)
				this.LookupFieldSelection = source[0];
			
			// get the view to respect this
			this.View.RefreshSelectionFields(source);
		}

		internal void ClearFilter()
		{
			if (this.Filter.Length > 0)
			{
				this.Filter = "";
				OnFilterChanged();
			}
		}

		///////////////////////////////////////////////////////////////////////////////////////////////
		// Helpers
		private void SetSourceFieldList(string table)
		{
			DbView.FieldInfo [] fields = this.infoIface.GetSchema(table);
			List<string> source = new List<string>();
			this.SourceFields.Clear();
			foreach (DbView.FieldInfo fi in fields)
			{
				source.Add(fi.Name);
				this.SourceFields.Add(fi.Name);
			}
			this.View.RefreshSourceField(source);
		}

		// This does everything
		private void SetSelectionFieldList(string table, bool keepSelectedFields)
		{
			DbView.FieldInfo [] fields = this.infoIface.GetSchema(table);
			List<string> lookupSource = new List<string>();
			List<string> source = new List<string>();
			if (!keepSelectedFields)
				this.newModel.SelectedFields.Clear();
			// always clear the source for the list
			this.LookupFields.Clear();
			foreach (DbView.FieldInfo fi in fields)
			{
				this.LookupFields.Add(fi.Name);
				if (!this.newModel.SelectedFields.Contains(fi.Name))
					source.Add(fi.Name);
				lookupSource.Add(fi.Name);
			}
			this.View.RefreshLookupField(lookupSource);
			if (source.Count > 0)
				this.LookupFieldSelection = source[0];
			this.View.RefreshSelectionFields(source);
			this.View.RefreshSelectedFields(this.newModel.SelectedFields);
			if (this.newModel.SelectedFields.Count > 0)
				this.SelectedLookupsSelection = this.newModel.SelectedFields[0];
			lastTT = this.newModel.LookupTable;
		}

		// remove items based on a name filter
		private List<string> FilterTheSelectionList(string filter)
		{
			return FilterTheSelectionList(filter, this.LookupFields, this.newModel.SelectedFields);
		}

		// more generic remove items based on a name filter
		private List<string> FilterTheSelectionList(string filter, List<string> selectionList, List<string> selectedList)
		{
		    // create a list to act as a new data-source
		    List<string> filteredList = new List<string>();
		    // check each item in the full list
		    foreach (string pi in selectionList)
		    {
		        // remove if in target list
		        if (selectedList.Contains(pi))
		            continue;
		        // remove if the filter is active
		        if (string.IsNullOrEmpty(filter) ||
		            pi.ToUpper().Contains(filter.ToUpper()))
		        {
		            filteredList.Add(pi);
		        }
		    }
		    return filteredList;
		}

		// figure out the selected item from the one just to be removed.
		private string FindNewSelection(List<string> list, string pi)
		{
			int delIdx = list.IndexOf(pi);
			bool isLastItem = (delIdx == list.Count - 1);
			// if not at end of list the item after the one to be deleted will be selected after
			// this keeps the cursor in position
			if (!isLastItem)
				return list[delIdx + 1];
			// if last item is the only one the don't set selection
			// else select the new last item
			return (delIdx == 0) ? null : list[delIdx - 1]; 
		}
	}
}
